/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.animation

import androidx.compose.ui.unit.Density
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.sign

/** Earth's gravity in SI units (m/s^2); used to compute deceleration based on friction. */
private const val GravityEarth = 9.80665f
private const val InchesPerMeter = 39.37f

/**
 * The default rate of deceleration for a fling if not specified in the [FlingCalculator]
 * constructor.
 */
private val DecelerationRate = (ln(0.78) / ln(0.9)).toFloat()

/**
 * Compute the rate of deceleration based on pixel density, physical gravity and a
 * [coefficient of friction][friction].
 */
private fun computeDeceleration(friction: Float, density: Float): Float =
    GravityEarth * InchesPerMeter * density * 160f * friction

/**
 * Configuration for Android-feel flinging motion at the given density.
 *
 * @param friction scroll friction.
 * @param density density of the screen. Use LocalDensity to get current density in composition.
 */
internal class FlingCalculator(private val friction: Float, val density: Density) {

    /** A density-specific coefficient adjusted to physical values. */
    private val magicPhysicalCoefficient: Float = computeDeceleration(density)

    /** Computes the rate of deceleration in pixels based on the given [density]. */
    private fun computeDeceleration(density: Density) = computeDeceleration(0.84f, density.density)

    private fun getSplineDeceleration(velocity: Float): Double =
        AndroidFlingSpline.deceleration(velocity, friction * magicPhysicalCoefficient)

    /** Compute the duration in milliseconds of a fling with an initial velocity of [velocity] */
    fun flingDuration(velocity: Float): Long {
        val l = getSplineDeceleration(velocity)
        val decelMinusOne = DecelerationRate - 1.0
        return (1000.0 * exp(l / decelMinusOne)).toLong()
    }

    /** Compute the distance of a fling in units given an initial [velocity] of units/second */
    fun flingDistance(velocity: Float): Float {
        val l = getSplineDeceleration(velocity)
        val decelMinusOne = DecelerationRate - 1.0
        return (friction * magicPhysicalCoefficient * exp(DecelerationRate / decelMinusOne * l))
            .toFloat()
    }

    /** Compute all interesting information about a fling of initial velocity [velocity]. */
    fun flingInfo(velocity: Float): FlingInfo {
        val l = getSplineDeceleration(velocity)
        val decelMinusOne = DecelerationRate - 1.0
        return FlingInfo(
            initialVelocity = velocity,
            distance =
                (friction * magicPhysicalCoefficient * exp(DecelerationRate / decelMinusOne * l))
                    .toFloat(),
            duration = (1000.0 * exp(l / decelMinusOne)).toLong(),
        )
    }

    /**
     * Info about a fling started with [initialVelocity]. The units of [initialVelocity] determine
     * the distance units of [distance] and the time units of [duration].
     */
    data class FlingInfo(val initialVelocity: Float, val distance: Float, val duration: Long) {
        fun position(time: Long): Float {
            val splinePos = if (duration > 0) time / duration.toFloat() else 1f
            return distance *
                sign(initialVelocity) *
                AndroidFlingSpline.flingPosition(splinePos).distanceCoefficient
        }

        fun velocity(time: Long): Float {
            val splinePos = if (duration > 0) time / duration.toFloat() else 1f
            return AndroidFlingSpline.flingPosition(splinePos).velocityCoefficient *
                sign(initialVelocity) *
                distance / duration * 1000.0f
        }
    }
}
